<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title id="title">STM32 ISP</title>
</head>

<body>
<textarea id="LogDisp" cols="60" style="height:16em" readonly> </textarea>
<br />
<progress id="IspProg" max="100", value="0"></progress>
<br />
<input type="file" id="LoadHexFile", accept=".hex"/>
<input type="button", id="download" value="download"/>

<p>
    STM32   ESP32 <br/>
    ------------- <br/>
    RST --- GPIO5 <br/>
    BOOT1 - GPIO12 <br/>
    PA9 --- GPIO25 <br/>
    PA10 -- GPIO26 <br/>
</p>
<script type="module">
import MemoryMap from "./intel-hex.js"

class WsGpio{
    static rst_low() {cmd_ws.send(REQ_GPIO5_L);}
    static rst_high() {cmd_ws.send(REQ_GPIO5_H);}
    static boot1_low() {cmd_ws.send(REQ_GPIO12_L);}
    static boot1_high() {cmd_ws.send(REQ_GPIO12_H);}
}

class Stm32F103Isp {
    // State
    STA_BOOTING = 0;
    STA_AUTO_BAUD = 10;
    STA_ERASE = 20;
    STA_WRITING = 30;
    STA_RUN = 40;
    STA_DONE = 90;  // success
    STA_FAILED = 100;

    // Sub State
    STA_ERASE_BEGIN = 21;
    STA_ERASE_WAIT = 22;

    STA_WR_START = 31
    STA_WR_SEND_CMD = 32;
    STA_WR_SEND_ADDR = 33;
    STA_WR_SEND_DAT = 34;
    STA_WR_ERR = 37;
    STA_WR_DONE = 38;
    
    // Parameter
    RETRY_TIMEOUT = 1000;   // ms
    ERASE_TIMEOUT = 5;      // sec
    WRITE_TIMEOUT = 2000;   // ms
    MAX_RETRIES = 2;

    // Bootloader Commands
    ACK = '79';
    NACK = '1F'

    constructor(hex_file) {
        this._state = this.STA_BOOTING;
        this._sub_state = undefined;
        this._retries = -1;  // how many time have tried
        this._interval_id = undefined;  // id for clearInterval()

        this._timer_s = 0;  // timer in second

        this._hex_memory_map = undefined
        this.load_hex_file(hex_file);  // assign result to _hex_memory_map asynchronously
    }
    // ===== helpers =====
    reset_retry() {
        this._retries = -1;
    }

    // Place this function at the begin of a task,
    // and reset retry when that task is done.
    retry_once_more(){
        this._retries++;
        Log("retry ", this._retries)
        if (this._retries >= this.MAX_RETRIES){
            this.reset_retry();
            this._state = this.STA_FAILED;
        }
    }

    async load_hex_file(hex_file) {
        var _reader = new FileReader();
        _reader.readAsText(hex_file, "utf-8");
        
        _reader.onload = () => {
            this._hex_memory_map = MemoryMap.fromHex(_reader.result);
        }
    }

    // ===== features =====
    download() {
        this._interval_id = setInterval(() => this.main_loop(), this.RETRY_TIMEOUT);
    }

    download_success() {
        Log("download success!");
        clearInterval(this._interval_id);
    }

    download_failed() {
        Log("download failed");
        clearInterval(this._interval_id);
    }

    reset_to_bootloader() {
        Log("RST拉低, BOOT1置1复位, STM32 进入 Bootloader");
        WsGpio.boot1_high(); delay_ms(500); WsGpio.rst_low(); 
        
        delay_ms(500);
        WsGpio.rst_high();
        this._state = this.STA_AUTO_BAUD;
    }

    reset_to_rom() {
        Log("RST拉低, BOOT1置0复位, STM32 从 Rom 启动");
        WsGpio.boot1_low(); delay_ms(500); WsGpio.rst_low(); 

        delay_ms(500);
        WsGpio.rst_high();

        this._state = this.STA_DONE;
    }

    autoBaud() {
        this.retry_once_more();
        Log("Auto Baud, send 0x7F ");
        uart_ws.send(BS([0x7F]));
    }
    handle_autoBaud(evt) {
        var _recv = evt.data;
        Log("autoBaud Recv " + _recv);

        if (_recv == this.ACK) {
            this._state = this.STA_ERASE;
            this.reset_retry();
        }
    }

    eraseFlash(){
        if (this._sub_state == undefined) {
            this._sub_state = this.STA_ERASE_BEGIN;
        }

        switch (this._sub_state){
            case this.STA_ERASE_BEGIN:
                this.retry_once_more();
                uart_ws.send(BS([0x43, 0xBC]));
                this._timer = this.ERASE_TIMEOUT;
                break;
            case this.STA_ERASE_WAIT:
                if (this._timer > 0) {
                    this._timer --;
                    return;
                }
                this._sub_state = this.STA_ERASE_BEGIN;  // timeout and retry
                break;
        }
    }
    handle_eraseFlash(evt){
        var _recv = evt.data;
        Log("erase recv ", _recv);

        switch (this._sub_state){
            case this.STA_ERASE_BEGIN:
                if (_recv == this.ACK){
                    uart_ws.send(BS([0xFF, 0x00]));  // global erase
                    this._sub_state = this.STA_ERASE_WAIT;
                }
                break;
            case this.STA_ERASE_WAIT:
                if (_recv == this.ACK) {
                    this._state = this.STA_WRITING;
                    this._sub_state = this.STA_WR_START;
                    this.reset_retry();
                }
                break;
        }
    }

    // use closure to share some variables in a smaller scope
    writeFlashStart() {
        if (this._hex_memory_map == undefined) {
            Log("wait reading hex to complete");
            return;
        }

        var _recv_timeout = new TimeoutHelper(this.WRITE_TIMEOUT);
        var self = this;  // to use this object in closure

        var SEND_SIZE = 64;
        var _memo_map = self._hex_memory_map, _memo_blk, _data;
        var _addresses = _memo_map.keys();
        var _addr_start, _offset;
        var _trans_retries = 0;

        var _total_size, _written_size;  // to calculate writing progress

        function writeFlashWatchDog() {
            if (_recv_timeout.is_timeout()) {
                Log("dog timeout");
                self._sub_state = self.STA_WR_ERR;
            }
        }

        function writeFlahsFeedDog(){_recv_timeout.reset();}

        function writeFlashInitData() {
            _addr_start = _addresses.next().value;  
            _memo_blk = _memo_map.get(_addr_start);  // Unit8Array
            _offset = 0;
            _data = _memo_blk.slice(_offset, _offset + SEND_SIZE);

            _total_size=0;
            _written_size=0
            for (var _addr of _memo_map.keys()){_total_size += _memo_map.get(_addr).length;}
        }

        function writeFlashUpateData() {
            _trans_retries = 0;

            _offset += SEND_SIZE;
            if (_offset >= _memo_blk.length){
                var _temp = _addresses.next();
                if (_temp.done) {
                    self._sub_state = self.STA_WR_DONE;
                    return;
                }

                _addr_start = _temp.value;
                _offset = 0;
                _memo_blk = _memo_map.get(_addr_start);                
            }
            _data = _memo_blk.slice(_offset, _offset + SEND_SIZE);
        }

        function sendArrWithChecksum(arr) {
            var arr = Array.from(arr);
            arr.push(checksum_xor(arr));

            uart_ws.send(BS(arr));
        }

        function sendData() {
            var arr = Array.from(_data);
            arr.splice(0, 0, _data.length-1);  // N bytes, refer to AN3155, chap 3.6
            sendArrWithChecksum(arr);

            // display progress
            _written_size += _data.length;
            SetIspProgress(_written_size, _total_size);
        }

        function writeFlashTransaction(){
            switch (self._sub_state) {
                case self.STA_WR_SEND_CMD: uart_ws.send(BS([0x31, 0xCE])); break;
                case self.STA_WR_SEND_ADDR: sendArrWithChecksum(int_to_buf_4bytes(_addr_start+_offset)); break;
                case self.STA_WR_SEND_DAT: sendData(); break;
                case self.STA_WR_ERR: 
                    if (_trans_retries > self.MAX_RETRIES) {self._state = self.STA_FAILED;}
                    _trans_retries++;
                    self._sub_state = self.STA_WR_CMD;
                    Log("wr err, retry ", _trans_retries);
                    break;
            }
        }

        self.writeFlashRoutine = function () {
            switch (self._sub_state) {
                case self.STA_WR_SEND_CMD: // watch dog
                case self.STA_WR_SEND_ADDR:  // watch dog
                case self.STA_WR_SEND_DAT: writeFlashWatchDog(); break;
                case self.STA_WR_ERR: self._sub_state = self.STA_FAILED; break;  // timeout
            }
        }

        self.handle_writeFlashTransaction = function (evt) {
            // Log("wr recv ", evt.data);
            writeFlahsFeedDog();

            // sub state transition
            if (evt.data == self.ACK) {
                _trans_retries = 0;

                switch (self._sub_state){
                    case self.STA_WR_SEND_CMD: self._sub_state = self.STA_WR_SEND_ADDR; break;
                    case self.STA_WR_SEND_ADDR: self._sub_state = self.STA_WR_SEND_DAT; break;
                    case self.STA_WR_SEND_DAT: 
                        self._sub_state = self.STA_WR_SEND_CMD;
                        writeFlashUpateData();
                        break;
                }
                writeFlashTransaction();
            }
            else {
                self._sub_state = self.STA_WR_ERR;
            }
        }

        // start writing
        Log("start writing flash...");
        writeFlashInitData();
        self._sub_state = self.STA_WR_SEND_CMD;
        writeFlashTransaction();  // trigger communication
    }

    writeFlash() {
        switch (this._sub_state) {
            case this.STA_WR_START: this.writeFlashStart(); break;
            case this.STA_WR_DONE: this._state = this.STA_RUN; break;
            default: this.writeFlashRoutine(); break;
        }
    }
    handle_writeFlash = function (evt) {
        this.handle_writeFlashTransaction(evt);
    }

    main_loop() {
        switch (this._state){
            case this.STA_BOOTING: this.reset_to_bootloader(); break;
            case this.STA_AUTO_BAUD: this.autoBaud(); break;
            case this.STA_ERASE: this.eraseFlash(); break;
            case this.STA_WRITING: this.writeFlash(); break;
            case this.STA_RUN: this.reset_to_rom(); break;
            case this.STA_DONE: this.download_success(); break;
            case this.STA_FAILED: this.download_failed(); break;
            default: Log('mainloop default')
        }
    }

    receiving_loop(evt) {
        switch (this._state){
            case this.STA_AUTO_BAUD: this.handle_autoBaud(evt); break;
            case this.STA_ERASE: this.handle_eraseFlash(evt); break;
            case this.STA_WRITING: this.handle_writeFlash(evt); break;
        }
    }
}

// ========================================
//   Gloabal Variables and Settings
// ========================================

// ===== Request data or command data =====
const REQ_UART = BS([0x10, 0x01]);   // use uart client
const REQ_CMD = BS([0x10, 0x02]);    // use command client
const REQ_GPIO5_H = BS([0x00, 5, 0x01])   // set GPIO5 high
const REQ_GPIO5_L = BS([0x00, 5, 0x00])   // set GPIO5 low
const REQ_GPIO12_H = BS([0x00, 12, 0x01])
const REQ_GPIO12_L = BS([0x00, 12, 0x00])

var cmd_ws = new WebSocket("ws://"+location.host);
var uart_ws = new WebSocket("ws://"+location.host);
// var cmd_ws = new WebSocket("ws://192.168.1.9");
// var uart_ws = new WebSocket("ws://192.168.1.9");
// uart_ws.binarytypie = 'arrayBuffer';

cmd_ws.onopen = function(evt) {
    Log("open cmd ws");
    this.send(REQ_CMD);
}

cmd_ws.onmessage = function(evt) {
    Log("cmd recv: ", evt.data);
}

cmd_ws.onclose = function(evt) {
    Log("close ws");
}

uart_ws.onopen = function(evt) {
    Log("open uart ws");
    this.send(REQ_UART);
}

uart_ws.onmessage = function(evt) {
    Log("uart recv: ", evt.data);
}


function Log(...msg) {
    var el = document.getElementById("LogDisp");
    var _all_msg = "";
    for (var i=0; i<msg.length; i++) {
        _all_msg += msg[i];
    }
    el.value += _all_msg + "\n";
    el.scrollTop = el.scrollHeight;  // move text to bottom
}

function SetIspProgress(val, total) {
    var el = document.getElementById("IspProg");
    el.value = val;
    el.max = total;
}

function Download() {
    var _files = document.getElementById("LoadHexFile").files;
    if (_files.length <= 0) {
        alert("请先选择 hex 文件");
        return;
    }

    Log("\n\ndownload");
    var isp = new Stm32F103Isp(_files[0]);
    uart_ws.onmessage = (evt) => isp.receiving_loop(evt);
    isp.download();
}

function CloseAllWebsocket(evt) {
    evt.preventDefault();
    Log("closing all websocket...");
    cmd_ws.close();
    uart_ws.close();
}

// ===== useful helpers =====
function delay_ms(t) {
    var _start = Date.now();
    while (Date.now() < (_start + t)) {}
}

function BS(arr) {
    return new Uint8Array(arr);
}

function checksum_xor(arr) {
    var _ck_sum = arr[0];
    for (var i=1; i<arr.length; i++) {
        _ck_sum ^= arr[i];
    }

    return _ck_sum;
}

// Convert int into its buffer of length of 4 bytes, big endian
function int_to_buf_4bytes(val) {
    var _hex_str = val.toString(16);
    var _buf = new Array();
    
    while (_hex_str.length < 8) {
        _hex_str = '0' + _hex_str;  // add padding zeros
    }

    for (var i=0; i<4; i++) {
        var _byte = _hex_str.slice(i*2, i*2+2);
        _buf.push(parseInt(_byte, 16));
    }

    return new Uint8Array(_buf);
}

class TimeoutHelper {
    constructor(timeout_in_ms=0) {
        this._start_time = Date.now();
        this._timeout = timeout_in_ms;
    }

    is_timeout() {
        return (this._start_time + this._timeout) < Date.now();
    }

    reset() {
        this._start_time = Date.now();
    }

    set_timeout(timeout_in_ms){this._timeout = timeout_in_ms;}
}


document.getElementById("download").addEventListener("click", Download);
window.addEventListener("beforeunload", CloseAllWebsocket);
</script>

</body>
</html>